<template>
  <div class="hello">
    <div>
      <input
        type="file"
        @change="handleFileChange"
        :disabled="status !== Status.wait"
      />
      <el-button
        size="mini"
        @click="handleUpload"
        :disabled="uploadDisabled"
      >上传
      </el-button>
      <el-button
        size="mini"
        @click="handleResume"
        v-if="status === Status.pause">恢复
      </el-button>
      <el-button
        v-else
        size="mini"
        :disabled="status !== Status.uploading || !container.hash"
        @click="handlePause"
      >暂停
      </el-button>
    </div>
    <div>
      <div>计算文件 hash</div>
      <el-progress :percentage="hashPercentage"></el-progress>
      <div>总进度</div>
      <el-progress :percentage="fakeUploadPercentage"></el-progress>
    </div>
    <el-table :data="data">
      <el-table-column
        prop="hash"
        label="切片hash"
        align="center"
      ></el-table-column>
      <el-table-column label="大小(KB)" align="center" width="120">
        <template v-slot="{ row }">
          {{ row.size | transformByte }}
        </template>
      </el-table-column>
      <el-table-column label="进度" align="center">
        <template v-slot="{ row }">
          <el-progress
            :percentage="row.percentage"
            color="#909399"
          ></el-progress>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
  import request from '../api/request'

  const SIZE = 10 * 1024 * 1024
  const Status = {
    wait: "wait",
    pause: "pause",
    uploading: "uploading"
  };

  export default {
    name: 'HelloWorld',
    data() {
      return {
        Status,
        container: {
          file: null,
          hash: '',
          worker: null
        },
        hashPercentage: 0,
        data: [],
        requestList: [],
        status: Status.wait,
        // 当暂停时会取消xhr导致进度条后退
        // 为了避免这种情况，需要定义一个假的进度条
        fakeUploadPercentage: 0
      }
    },
    filters: {
      transformByte(val) {
        return Number((val / 1024).toFixed(0))
      }
    },
    created() {
      console.log([Status.pause, Status.uploading])
    },
    computed: {
      uploadDisabled() {
        return (
          !this.container.file || [Status.pause, Status.uploading].includes(this.status)
        )
      },
      uploadPercentage() {
        if (!this.container.file || !this.data.length) return 0
        const loaded = this.data.map(item => item.size * item.percentage).reduce((acc, cur) => acc + cur)
        return parseInt((loaded / this.container.file.size).toFixed(2))
      }
    },
    watch: {
      uploadPercentage(now) {
        if (now > this.fakeUploadPercentage) {
          this.fakeUploadPercentage = now
        }
      }
    },
    methods: {
      handlePause() {
        this.status = Status.pause
        this.resetData()
      },
      resetData() {
        this.requestList.forEach(xhr => xhr.abort())
        this.requestList = []
        if (this.container.worker) {
          this.container.worker.onmessage = null
        }
      },
      async handleResume() {
        this.status = Status.uploading
        const {uploadedList} = await this.verifyUpload(this.container.file.name, this.container.hash)
        await this.uploadChunks(uploadedList)
      },
      handleFileChange(e) {
        const [file] = e.target.files
        if (!file) return
        Object.assign(this.$data, this.$options.data())
        this.container.file = file
      },
      // 生成文件切片
      createFileChunk(file, size = SIZE) {
        const fileChunkList = []
        let cur = 0
        while (cur < file.size) {
          fileChunkList.push({
            file: file.slice(cur, cur + size)
          })
          cur += size
        }
        return fileChunkList
      },
      // 上传切片, 同时过滤已上传的切片
      async uploadChunks(uploadedList = []) {
        const requestList = this.data
          .filter(({hash}) => !uploadedList.includes(hash))
          .map(({chunk, hash, index}) => {
            const formData = new FormData();
            formData.append("chunk", chunk);
            formData.append("hash", hash);
            formData.append("filename", this.container.file.name);
            formData.append("fileHash", this.container.hash);
            return {formData, index};
          })
          .map(async ({formData, index}) =>
            request({
              url: "http://localhost:3000",
              data: formData,
              onProgress: this.createProgressHandler(this.data[index]),
              requestList: this.requestList
            })
          )
        // 并发切片
        await Promise.all(requestList)
        // 之前上传的切片数量 + 本次上传的切片数量 = 所有切片数量时
        // 合并切片
        if (uploadedList.length + requestList.length === this.data.length) {
          await this.mergeRequest();
        }
      },
      async mergeRequest() {
        await request({
          url: 'http://localhost:3000/merge',
          headers: {
            'content-type': 'application/json'
          },
          data: JSON.stringify({
            size: SIZE,
            filename: this.container.file.name,
            fileHash: this.container.hash
          })
        })
        this.$message.success("上传成功")
        this.status = Status.wait
      },
      // 根据 hash 验证文件是否曾经已经被上传过
      // 没有才进行上传
      async verifyUpload(filename, fileHash) {
        const {data} = await request({
          url: 'http://localhost:3000/verify',
          headers: {
            "content-type": "application/json"
          },
          data: JSON.stringify({
            filename,
            fileHash
          })
        })
        return JSON.parse(data)
      },
      // 生成hash（web-worker）
      calculateHash(fileChunkList) {
        return new Promise(resolve => {
          this.container.worker = new Worker('/hash.js')
          this.container.worker.postMessage({fileChunkList})
          this.container.worker.onmessage = e => {
            const {percentage, hash} = e.data
            this.hashPercentage = percentage
            if (hash) {
              resolve(hash)
            }
          }
        })
      },
      async handleUpload() {
        if (!this.container.file) return
        this.status = Status.uploading
        const fileChunkList = this.createFileChunk(this.container.file)
        this.container.hash = await this.calculateHash(fileChunkList)

        const {shouldUpload, uploadedList} = await this.verifyUpload(this.container.file.name, this.container.hash)
        if (!shouldUpload) {
          this.$message.success('秒传：上传成功！')
          this.status = Status.wait
          return;
        }

        this.data = fileChunkList.map(({file}, index) => ({
          fileHash: this.container.hash,
          chunk: file,
          index,
          hash: this.container.file.name + "-" + index,
          size: file.size,
          percentage: uploadedList.includes(index) ? 100 : 0
        }));
        await this.uploadChunks(uploadedList)
      },
      // 用闭包保存每个chunk的进度数据
      createProgressHandler(item) {
        return e => {
          item.percentage = parseInt(String(e.loaded / e.total) * 100)
        }
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  h3 {
    margin: 40px 0 0;
  }

  ul {
    list-style-type: none;
    padding: 0;
  }

  li {
    display: inline-block;
    margin: 0 10px;
  }

  a {
    color: #42b983;
  }
</style>
